#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Time      :2022/6/1 15:17
# @Author    :cjw
import os
import socket
import time
import subprocess
import multiprocessing

from app_wework.setting import LOG_PATH
from app_wework.util.log import logger


class AppiumDeviceSync:
    """
    appium服务和设备并发执行，暂时只做了windows10 安卓的功能
    """
    # 格式化的当前时间
    now_time = time.strftime('%Y_%m_%d_%H_%M_%S')
    # 格式化的当前日期
    now_date = time.strftime('%Y_%m_%d')

    @classmethod
    def check_port_is_use(cls, port: int, host: str = '127.0.0.1') -> bool:
        """
        端口检查，通过socket连接appium服务来判断端口是否已经被占用
        socket: https://docs.python.org/3.9/library/socket.html

        shutdown方法：禁止在一个Socket上进行数据的接收与发送。
        利用shutdown()函数使socket双向数据传输变为单向数据传输。
        shutdown()需要一个单独的参数，该参数表示了如何关闭socket
        参数：
        0：表示禁止将来读
        1：表示禁止将来写
        2：表示禁止将来读和写
        :param port: 检查的端口
        :param host: 检查的端口地址，默认是本地
        :return: True: 被占用，False：未被占用
        """
        # AF_INET: 使用ipv4协议通信
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            logger.info(f'开始进行端口检查，检查ip：{host}, 检查端口：{port}')
            s.connect((host, port))
            s.shutdown(2)  # 关闭连接
        except OSError as e:  # 连接出现异常，表示端口没有被占用
            logger.info(f'端口：{port}，未被占用')
            return False
        else:
            logger.info(f'端口：{port}，已经被占用')
            return True

    @classmethod
    def release_port(cls, port: int) -> None:
        """
        释放端口
        :param port: 端口
        :return:
        """
        # 根据要释放的端口查找进程pid的命令
        find_command = f'netstat -ano | findstr {port}'
        logger.info(f'查找进程端口命令：{find_command}')
        # 执行命令获取pid
        result = os.popen(find_command).read()
        logger.info(f'进程pid查找结果：\n{result}')

        # 查找进程pid, 存在会有LISTENING 监听的pid
        if str(port) and 'LISTENING' in result:
            # 获取端口对应的pid
            i = result.index('LISTENING')
            start = i + len('LISTENING') + 7
            end = result.index('\n')
            pid = result[start:end]
            logger.info(f'获取的进程pid：{pid}')

            # 释放端口对应的pid
            kill_command = f'taskkill -f -pid {pid}'
            logger.info(f'端口释放命令：{kill_command}')
            os.popen(kill_command)
            logger.info(f'端口：{port}，释放完成')
        else:
            logger.info(f'端口：{port}，是可用的')

    @classmethod
    def appium_server_start(cls, port: int, host: str = '127.0.0.1') -> None:
        """
        启动appium服务，通过subprocess模块启动
        subprocess: https://docs.python.org/3.9/library/subprocess.html
        该模块可以创建新的进程，并且连接到进程的输入、输出、错误等管道信息，并且可以获取进程的返回值。

        appium服务启动参数：
        -bp(--bootstrap-port): appium和设备之间通信的端口，如果不指定到时无法操作多台设备运行脚本。
        --relaxed-security: 禁用其他安全检查
        --session-override: 启用会话覆盖（关闭）
        :param port: 启动端口
        :param host: 启动端口的ip，默认是0.0.0.0
        :return:
        """
        # appium服务启动命令：加上start /b 表示后台运行
        cmd = f'start /b appium -a {host} -p {port} -bp {port + 1} ' \
              f'--session-override --relaxed-security'
        logger.info(f'appium服务启动命令：{cmd}')

        # 动态生成日志文件，重启服务后每天生成一个日志文件
        log_path = os.path.join(LOG_PATH, 'appium', f'{port}')
        # 目录不存在，就重新创建
        if not os.path.isdir(log_path):
            # 如果appium和port目录都不存在，创建多级目录需要使用makedirs，否则会报错
            os.makedirs(log_path)
        log_path = os.path.join(log_path, f'{cls.now_date}.log')

        # 执行命令，并把日志打印到指定的日志文件中
        logger.info(f'启动appium服务：http://{host}:{port}/wb/hub')
        subprocess.Popen(cmd, shell=True, stdout=open(log_path, 'a', encoding='utf-8'),
                         stderr=subprocess.STDOUT, encoding='utf-8')

    @classmethod
    def appium_server_restart(cls, port: int, host: str = '127.0.0.1') -> None:
        """
        重启appium服务
        :param port: 启动端口
        :param host: 启动IP，默认是0.0.0.0
        :return:
        """
        logger.info(f'重启appium服务：http://{host}:{port}/wb/hub')
        # 首先需要释放启动服务的端口
        cls.release_port(port)
        # 端口释放以后，再启动appium服务
        cls.appium_server_start(port, host)

    @classmethod
    def appium_server_action(cls, port: int, host: str = '127.0.0.1') -> None:
        """
        appium服务启动操作：如果端口未被占用，直接启动；如果端口被占用，则重启appium服务
        :param port: 启动端口
        :param host: 启动IP，默认是0.0.0.0
        :return:
        """
        # 判断端口状态，再选择启动方式
        if cls.check_port_is_use(port, host):
            cls.appium_server_restart(port, host)
        else:
            cls.appium_server_start(port, host)

    @classmethod
    def devices(cls) -> list:
        """获取当前电脑上连接成功的设备序列号列表"""
        # 获取执行设备查询命令的结果里列表，去除首位两行
        result = os.popen(r'adb devices').readlines()[1:-1]
        # 过滤非device状态的设备序列号
        devices = [device for device in result if 'device' in device]
        devices_list = [device.split('\t')[0] for device in devices]
        logger.info(f'获取到的设备列表：{devices_list}')
        return devices_list

    @classmethod
    def appium_server_start_sync(cls, host: str = '127.0.0.1') -> bool:
        """
        并发启动appium服务
        :param host: 启动appium服务的IP
        :return:
        """
        logger.info('============开始并发启动appium服务==============')

        # 创建appium进程组
        appium_process = []
        # 获取设备udid列表
        device_list = cls.devices()

        flag = False  # 启动标识
        if device_list:  # 判断是否并发启动
            # 加载appium进程组
            for i in range(len(device_list)):
                port = 4723 + 2 * i
                appium = multiprocessing.Process(target=cls.appium_server_action, args=(port, host))
                appium_process.append(appium)

            # 启动appium服务
            for appium in appium_process:
                appium.start()
            for appium in appium_process:
                appium.join()
            # 给个时间缓冲一下
            time.sleep(2)
            logger.info('并发启动成功')
            flag = True
        else:
            logger.info('没有查找到任何设备，并发启动失败')
        logger.info('============结束并发启动appium服务==============')
        return flag


if __name__ == '__main__':
    # print(AppiumDeviceSync.check_port_is_use(4723))
    # AppiumDeviceSync.release_port(4723)
    # AppiumDeviceSync.appium_server_start(4723)
    AppiumDeviceSync.appium_server_start_sync()
